package Harmonisation.CreationDuChant;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;

import Exception.ArbreCheminBloqueException;
import Harmonisation.Accord;
import Harmonisation.Chant;
import Harmonisation.EnsembleNote;
import Harmonisation.Note;
import Harmonisation.CreationDuChant.Arbre.Arbre;
import Harmonisation.CreationDuChant.Arbre.Noeud;
import Harmonisation.Voix.Alto;
import Harmonisation.Voix.Basse;
import Harmonisation.Voix.Tenor;

public class GrapheNotes {
	private ArrayList<Note> soprano;
	private Arbre<EnsembleNote> arbre;
	private static Hashtable<Integer, Note> hashNote = new Hashtable<Integer, Note>() {
		private static final long serialVersionUID = 8085905424403384730L;
		{

			// Premiere octave
			put(0, new Note("do1", 1));
			put(1, new Note("re1", 1));
			put(2, new Note("mi1", 1));
			put(3, new Note("fa1", 1));
			put(4, new Note("sol1", 1));
			put(5, new Note("la1", 1));
			put(6, new Note("si1", 1));
			// Deuxieme octave
			put(7, new Note("do2", 1));
			put(8, new Note("re2", 1));
			put(9, new Note("mi2", 1));
			put(10, new Note("fa2", 1));
			put(11, new Note("sol2", 1));
			put(12, new Note("la2", 1));
			put(13, new Note("si2", 1));
			// Troisieme octave

			put(14, new Note("do3", 1));
			put(15, new Note("re3", 1));
			put(16, new Note("mi3", 1));
			put(17, new Note("fa3", 1));
			put(18, new Note("sol3", 1));
			put(19, new Note("la3", 1));
			put(20, new Note("si3", 1));
			// Quatrieme octave
			put(21, new Note("do4", 1));
			put(22, new Note("re4", 1));
			put(23, new Note("mi4", 1));
			put(24, new Note("fa4", 1));
			put(25, new Note("sol4", 1));
			put(26, new Note("la4", 1));
			put(27, new Note("si4", 1));
		}
	};

	private ArrayList<Accord> listAccord;

	public GrapheNotes(ArrayList<Note> soprano, ArrayList<ArrayList<Accord>> accordPossibleAChaqueTemps) throws ArbreCheminBloqueException {
		this.arbre = new Arbre<EnsembleNote>();
		this.soprano = soprano;

		listAccord = new ArrayList<Accord>();
		/* creation des accords */
		Accord accord1 = new Accord("do", "mi", "sol", "i");
		Accord accord2 = new Accord("re", "fa", "la", "ii");
		Accord accord3 = new Accord("mi", "sol", "si", "iii");
		Accord accord4 = new Accord("fa", "la", "do", "iv");
		Accord accord4b = new Accord("fa", "la", "do", "ivb");
		Accord accord5 = new Accord("sol", "si", "re", "v");
		Accord accord6 = new Accord("la", "do", "mi", "vi");
		Accord accord7 = new Accord("si", "re", "fa", "vii");

		accord1.getEnchainementPossible().add(accord1);
		accord1.getEnchainementPossible().add(accord2);
		accord1.getEnchainementPossible().add(accord3);
		accord1.getEnchainementPossible().add(accord4);
		accord1.getEnchainementPossible().add(accord5);
		accord1.getEnchainementPossible().add(accord6);
		accord1.getEnchainementPossible().add(accord7);

		accord2.getEnchainementPossible().add(accord2);
		accord2.getEnchainementPossible().add(accord5);
		accord2.getEnchainementPossible().add(accord7);

		accord3.getEnchainementPossible().add(accord2);
		accord3.getEnchainementPossible().add(accord3);
		accord3.getEnchainementPossible().add(accord4);
		accord3.getEnchainementPossible().add(accord5);
		accord3.getEnchainementPossible().add(accord6);
		accord3.getEnchainementPossible().add(accord7);

		accord4.getEnchainementPossible().add(accord1);
		accord4.getEnchainementPossible().add(accord2);
		accord4.getEnchainementPossible().add(accord3);
		accord4.getEnchainementPossible().add(accord4);
		accord4.getEnchainementPossible().add(accord5);
		accord4.getEnchainementPossible().add(accord6);
		accord4.getEnchainementPossible().add(accord7);

		accord4b.getEnchainementPossible().add(accord1);
		accord4b.getEnchainementPossible().add(accord4b);

		accord5.getEnchainementPossible().add(accord1);
		accord5.getEnchainementPossible().add(accord3);
		accord5.getEnchainementPossible().add(accord5);
		accord5.getEnchainementPossible().add(accord6);
		accord5.getEnchainementPossible().add(accord4b);

		accord6.getEnchainementPossible().add(accord2);
		accord6.getEnchainementPossible().add(accord3);
		accord6.getEnchainementPossible().add(accord4);
		accord6.getEnchainementPossible().add(accord5);
		accord6.getEnchainementPossible().add(accord6);

		accord7.getEnchainementPossible().add(accord1);
		accord7.getEnchainementPossible().add(accord3);
		accord7.getEnchainementPossible().add(accord7);

		listAccord.add(accord1);
		listAccord.add(accord2);
		listAccord.add(accord3);
		listAccord.add(accord4);
		listAccord.add(accord4b);
		listAccord.add(accord5);
		listAccord.add(accord6);
		listAccord.add(accord7);

		Note noteSoprano = null;
		int tempsSoprano = 0;
		Iterator<Note> itNoteSoprano = soprano.iterator();
		Iterator<ArrayList<Accord>> itAccord = accordPossibleAChaqueTemps.iterator();
		while ((itAccord.hasNext() && itNoteSoprano.hasNext()) || (tempsSoprano > 0)) {
			ArrayList<Noeud<EnsembleNote>> ensembleNoteNoeud = this.arbre.obtenirDernierElement();/* est nulle a la premiere iteration */
			ArrayList<Noeud<EnsembleNote>> nouvelEnsembleNoteNoeud = new ArrayList<Noeud<EnsembleNote>>();
			ArrayList<Accord> accordPossible = itAccord.next();
			if (tempsSoprano == 0) {
				noteSoprano = itNoteSoprano.next();
				tempsSoprano = noteSoprano.getTemps();

			}
			ajoutBasse(accordPossible, ensembleNoteNoeud, GrapheNotes.hashNote.get(noteSoprano.noteAuFormatNombre()), nouvelEnsembleNoteNoeud);
			tempsSoprano--;
			this.arbre.ajouter(nouvelEnsembleNoteNoeud);
		}
	}

	private void ajoutBasse(ArrayList<Accord> accordPossible, ArrayList<Noeud<EnsembleNote>> ensembleNoteNoeud, Note noteSoprano, ArrayList<Noeud<EnsembleNote>> nouvelEnsembleNoteNoeud) {
		int compteurBasse = Basse.noteMin;
		while (compteurBasse <= Basse.noteMax) {
			if (regleLocalAjoutBasse(GrapheNotes.hashNote.get(compteurBasse), accordPossible)) {
				ajoutTenor(GrapheNotes.hashNote.get(compteurBasse), ensembleNoteNoeud, noteSoprano, nouvelEnsembleNoteNoeud);
			}
			compteurBasse++;
		}
	}

	private boolean regleLocalAjoutBasse(Note note, ArrayList<Accord> accordPossible) {
		Iterator<Accord> it = this.listAccord.iterator();
		while (it.hasNext()) {
			Accord accord = it.next();
			if (accord.estTonique(note)) {
				return true;
			}
		}

		return false;

	}

	private void ajoutTenor(Note noteBasse, ArrayList<Noeud<EnsembleNote>> ensembleNoteNoeud, Note noteSoprano, ArrayList<Noeud<EnsembleNote>> nouvelEnsembleNoteNoeud) {
		int compteurTenor = Tenor.noteMin;
		while (compteurTenor <= Tenor.noteMax) {
			if (regleLocalAjoutTenor(GrapheNotes.hashNote.get(compteurTenor), noteBasse, noteSoprano)) {
				ajoutAlto(GrapheNotes.hashNote.get(compteurTenor), ensembleNoteNoeud, noteSoprano, noteBasse, nouvelEnsembleNoteNoeud);
			}
			compteurTenor++;
		}
	}

	private boolean regleLocalAjoutTenor(Note noteTenor, Note noteBasse, Note noteSoprano) {

		/*
		 * Pour que la regle 5 soit respectée pour une note quelconque, il faut que le soprano joue un note de l'accord sauf la tonique, si la note du tenor
		 * appartient a l'accord, il n'est pas necessaire au soprano de jouer une note autre que la tonique
		 */
		if (trouverAccordParTonique(noteBasse).estTonique(noteSoprano)) {
			if (!((trouverAccordParTonique(noteBasse).estQuinte(noteTenor) || trouverAccordParTonique(noteBasse).estTierce(noteTenor)))) {
				return false;
			}
		}

		if (!verifRegle6(noteTenor, noteBasse, trouverAccordParTonique(noteBasse))) {
			return false;
		}

		if (!verifRegle2(noteTenor, noteBasse)) {
			return false;
		}

		return true;
	}

	private void ajoutAlto(Note noteTenor, ArrayList<Noeud<EnsembleNote>> ensembleNoteNoeud, Note noteSoprano, Note noteBasse, ArrayList<Noeud<EnsembleNote>> nouvelEnsembleNoteNoeud) {
		int compteurAlto = Alto.noteMin;
		while (compteurAlto < Alto.noteMax) {
			if (regleLocalAjoutAlto(GrapheNotes.hashNote.get(compteurAlto), noteTenor, noteBasse, noteSoprano)) {
				EnsembleNote newEnsemble = new EnsembleNote(noteBasse, noteTenor, GrapheNotes.hashNote.get(compteurAlto), noteSoprano);
				Noeud<EnsembleNote> newEnsembleNoeud = new Noeud<EnsembleNote>(newEnsemble);
				if (ensembleNoteNoeud != null) {
					if (regleEnchainement(newEnsembleNoeud, ensembleNoteNoeud)) {
						nouvelEnsembleNoteNoeud.add(newEnsembleNoeud);
					}
				} else {
					nouvelEnsembleNoteNoeud.add(newEnsembleNoeud);
				}
			}
			compteurAlto++;
		}
	}

	private boolean regleLocalAjoutAlto(Note noteAlto, Note noteTenor, Note noteBasse, Note noteSoprano) {
		/*
		 * Pour que la regle soit respectée pour une note quelconque, il faut que le soprano et l'alto jouent la tierce ou la quinte, si la note du tenor
		 * appartient a l'accord, il est necessaire que le soprano ou le tenor jouent la note restante de l'accord(tierce ou quinte)
		 */

		if (!accordReconstitue(noteBasse, noteTenor, noteAlto, noteSoprano)) {
			return false;
		}

		if (!verifRegle6(noteAlto, noteTenor, trouverAccordParTonique(noteBasse))) {
			return false;
		}

		if (!verifRegle2(noteSoprano, noteAlto)) {
			return false;
		}

		if (!verifRegle2(noteAlto, noteTenor)) {
			return false;
		}

		if (!(trouverAccordParTonique(noteBasse)).appartientALAccord(noteSoprano)) {
			return false;
		}

		return true;
	}

	private boolean regleEnchainement(Noeud<EnsembleNote> newEnsembleNoeud, ArrayList<Noeud<EnsembleNote>> ensembleNoteNoeud) {
		boolean aPredecesseur = false;
		Iterator<Noeud<EnsembleNote>> itEnsembleNote = ensembleNoteNoeud.iterator();
		while (itEnsembleNote.hasNext()) {
			Noeud<EnsembleNote> ensembleNote = itEnsembleNote.next();
			if (regleEnchainement(newEnsembleNoeud.getElement(), ensembleNote.getElement())) {
				aPredecesseur = true;
				ensembleNote.ajouterVoisin(newEnsembleNoeud);
			}
		}
		return aPredecesseur;
	}

	private boolean regleEnchainement(EnsembleNote newEnsemble, EnsembleNote ensembleParent) {
		/* regle d'enchainement des accord */
		if (!trouverAccordParTonique(ensembleParent.getBasse()).EstVoisin(trouverAccordParTonique(newEnsemble.getBasse()))) {
			return false;
		}
		/* regle 6 */
		if (!verifRegle6Enchainement(newEnsemble.getAlto(), ensembleParent.getAlto(), trouverAccordParTonique(newEnsemble.getBasse()))) {
			return false;
		}
		if (!verifRegle6Enchainement(newEnsemble.getTenor(), ensembleParent.getTenor(), trouverAccordParTonique(newEnsemble.getBasse()))) {
			return false;
		}
		if (!verifRegle6Enchainement(newEnsemble.getBasse(), ensembleParent.getBasse(), trouverAccordParTonique(newEnsemble.getBasse()))) {
			return false;
		}

		return true;
	}

	public Chant genererChantAleatoire() {
		return new Chant(this.arbre.genererCheminAleatoire(), this.soprano);
	}

	private boolean verifRegle2(Note note, Note notePere) {/*
															 * Règles 2 (local) : A chaque instant, la note de basse est plus petite que celle du ténor, celle du ténor
															 * plus petit que celle de l’alto et celle de l’alto plus petit que celle du soprano.
															 */
		return note.noteAuFormatNombre() > notePere.noteAuFormatNombre();
	}

	private boolean verifRegle6(Note noteNew, Note noteInstrumentParent, Accord accordNew) {

		/*
		 * A un instant donnée et pour un chant donnée, la différence entre les
		 * deux notes ne peut pas excéder 6.
		 */
		if (noteNew.ecartEntreDeuxNote(noteInstrumentParent) > 6) {
			return false;
		}

		/*
		 * Quand la différence entre les deux notes excèdent 2 (strictement), elles doivent être de
		 * même nature (tonique, tierce ou quinte) dans leurs accords respectifs.
		 */
		if (noteNew.ecartEntreDeuxNote(noteInstrumentParent) > 2) {
			if (!(accordNew.appartientALAccord(noteNew))) {
				return false;
			}

		}
		return true;
	}

	private boolean verifRegle6Enchainement(Note noteNew, Note noteParent, Accord accordNew) {
		/*
		 * Règles 6 (enchainement) :
		 * /*De plus, si la note appartient à l’accord suivant, elle ne doit pas changer.
		 */
		if (accordNew.appartientALAccord(noteParent)) {
			if (!noteNew.equals(noteParent)) {
				return false;
			}
		}

		return true;
	}

	private boolean accordReconstitue(Note noteBasse, Note noteTenor, Note noteAlto, Note noteSoprano) {
		Accord accord = trouverAccordParTonique(noteBasse);
		if (!(accord.estQuinte(noteTenor) || accord.estQuinte(noteAlto) || accord.estQuinte(noteSoprano))) {
			return false;
		}
		if (!(accord.estTierce(noteTenor) || accord.estTierce(noteAlto) || accord.estTierce(noteSoprano))) {
			return false;
		}
		return true;
	}

	/**
	 * Calcul le nombre d'harmonisation total du chant
	 * 
	 * @return nombre d'harmonisation
	 */
	public long nombreHarmonisation() {
		long nb = 0;
		HashMap<Noeud<EnsembleNote>, Long> nbHarmonisation = new HashMap<Noeud<EnsembleNote>, Long>();

		/* initialisation, on met toutes les feuille a 1 */
		Iterator<Noeud<EnsembleNote>> it = this.arbre.obtenirDernierElement().iterator();
		while (it.hasNext()) {
			Noeud<EnsembleNote> noeud = it.next();
			nbHarmonisation.put(noeud, 1l);

		}
		/* on calcul tout les noeuds sauf ceux contenu dans la premiere arraylist de l'arbre */
		int i = this.arbre.tailleArbre() - 2;
		while (i > 0) {
			it = this.arbre.obtenirElement(i).iterator();
			while (it.hasNext()) {
				Noeud<EnsembleNote> noeud = it.next();
				nbHarmonisation.put(noeud, sommeNbHarmonisationSuivant(noeud, nbHarmonisation));
			}
			i--;
		}

		/* on a calculé toute les harmonisations pour les noeuds de 1 a n, on recupere maintenant la somme des harmo des noeud contenue dans l'array 0 */
		it = this.arbre.obtenirElement(0).iterator();
		while (it.hasNext()) {
			Noeud<EnsembleNote> noeud = it.next();
			if (this.arbre.size() > 0) {
				nb += sommeNbHarmonisationSuivant(noeud, nbHarmonisation);
			} else {
				nb++;
			}
		}
		return nb;
	}

	private long sommeNbHarmonisationSuivant(Noeud<EnsembleNote> noeud, HashMap<Noeud<EnsembleNote>, Long> nbHarmonisation) {
		long nb = 0;
		Iterator<Noeud<EnsembleNote>> it = noeud.getSuivant().iterator();
		while (it.hasNext()) {
			Noeud<EnsembleNote> noeudSuivant = it.next();

			nb += nbHarmonisation.get(noeudSuivant);
		}
		return nb;
	}

	private Accord trouverAccordParTonique(Note n) {
		Accord a;
		Iterator<Accord> it = this.listAccord.iterator();
		while (it.hasNext()) {
			a = it.next();
			if (a.estTonique(n)) {
				return a;
			}

		}
		return null;
	}

	/**
	 * 
	 * @param facteurBeaute
	 *            int 1/2/3/4 qui décrit le methode pour detecter le chant voulu
	 * @return le chant correspondant le mieux au parametre
	 */
	public Chant genererPlusBelleHarmonie(int facteurBeaute) {
		Chant c = null;
		HashMap<Noeud<EnsembleNote>, Integer> nbBeaute = new HashMap<Noeud<EnsembleNote>, Integer>();

		/* initialisation, on calcule pour tout les feuilles la valeur de beaute initiale */
		Iterator<Noeud<EnsembleNote>> it = this.arbre.obtenirDernierElement().iterator();
		while (it.hasNext()) {
			Noeud<EnsembleNote> noeud = it.next();
			nbBeaute.put(noeud, calculerBeaute(noeud.getElement(), null, facteurBeaute));

		}
		/* on calcule maintenant tout les noeuds de l'arbre */
		int i = this.arbre.tailleArbre() - 1;
		while (i >= 0) {
			it = this.arbre.obtenirElement(i).iterator();
			while (it.hasNext()) {
				Noeud<EnsembleNote> noeud = it.next();
				nbBeaute.put(noeud, facteurBeauteMax(nbBeaute, noeud, facteurBeaute));
			}
			i--;
		}

		/* on a calculé toute les harmonisations pour les noeuds de 1 a n, on recupere maintenant la somme des harmo des noeud contenue dans l'array 0 */
		Noeud<EnsembleNote> noeudBeauteMax = chercherMaxBeauteDansList(nbBeaute, this.arbre.obtenirElement(0));

		/* il suffit maintenant de remonter le chemin des suivant de ce noeud en prenant le max a chaque fois */
		ArrayList<EnsembleNote> array = new ArrayList<EnsembleNote>();
		array.add(noeudBeauteMax.getElement());
		while (noeudBeauteMax != null) {
			noeudBeauteMax = chercherMaxBeauteDansList(nbBeaute, noeudBeauteMax.getSuivant());
			if (noeudBeauteMax != null) {
				array.add(noeudBeauteMax.getElement());
			}
		}

		c = new Chant(array, soprano);
		return c;
	}

	private int facteurBeauteMax(HashMap<Noeud<EnsembleNote>, Integer> nbBeaute, Noeud<EnsembleNote> noeud, int facteurBeaute) {
		Iterator<Noeud<EnsembleNote>> it = noeud.getSuivant().iterator();
		int max = 0, beauteGenere, beauteDuSuivant;
		while (it.hasNext()) {
			Noeud<EnsembleNote> noeudSuivant = it.next();
			beauteGenere = calculerBeaute(noeud.getElement(), noeudSuivant.getElement(), facteurBeaute);
			beauteDuSuivant = nbBeaute.get(noeudSuivant);
			if ((beauteGenere + beauteDuSuivant) > max) {
				max = beauteGenere + beauteDuSuivant;
			}
		}
		return max;
	}

	private int calculerBeaute(EnsembleNote ensembleNote, EnsembleNote ensembleNote2, int facteurBeaute) {
		int beauteGenere = 0;
		if (facteurBeaute == 1 || facteurBeaute == 4)/* privilegie le faible ecart entre l'alto et le tenor */
		{/* on decide de soustraire l'écart entre les deux notes*3 */
			beauteGenere = beauteGenere - 4 * ensembleNote.getAlto().ecartEntreDeuxNote(ensembleNote.getTenor());
		}
		if (facteurBeaute == 2 || facteurBeaute == 4)/* privilegie la tenue de notes alto/tenor */
		{/* on decide arbitrairement que l'on ajoute 3 points pour la tenue d'une note */

			if (ensembleNote2 != null) {
				if (ensembleNote.getTenor().equals(ensembleNote2.getTenor())) {
					beauteGenere += 3;
				}
				if (ensembleNote.getAlto().equals(ensembleNote2.getAlto())) {
					beauteGenere += 3;
				}
			}
		}
		if (facteurBeaute == 3 || facteurBeaute == 4)/* privilegie l'amplitude de la basse */
		{/* on decide arbitrairement que l'on ajoute 2 points multiplie par la difference de nombre de note */
			if (ensembleNote2 != null) {
				beauteGenere += 2 * ensembleNote.getBasse().ecartEntreDeuxNote(ensembleNote2.getBasse());
			}
		}
		return beauteGenere;
	}

	private Noeud<EnsembleNote> chercherMaxBeauteDansList(HashMap<Noeud<EnsembleNote>, Integer> nbBeaute, ArrayList<Noeud<EnsembleNote>> array) {
		Noeud<EnsembleNote> noeudBeauteMax = null;
		int max = 0;
		Iterator<Noeud<EnsembleNote>> it = array.iterator();
		while (it.hasNext()) {
			Noeud<EnsembleNote> noeud = it.next();
			if (nbBeaute.get(noeud) >= max) {
				noeudBeauteMax = noeud;
				max = nbBeaute.get(noeud);
			}
		}
		return noeudBeauteMax;
	}

}
